/******************************************************************************

	COPYRIGHT(C) JONAS 'SORTIE' TERMANSEN 2010.

	This file is part of the Maxsi Library.

	Maxsi Library is free software: you can redistribute it and/or modify it
	under the terms of the GNU Lesser General Public License as published by
	the Free Software Foundation, either version 3 of the License, or (at your
	option) any later version.

	Maxsi Library is distributed in the hope that it will be useful, but
	WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
	or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
	License for more details.

	You should have received a copy of the GNU Lesser General Public License
	along with Maxsi Library. If not, see <http://www.gnu.org/licenses/>.

	Maxsi Library
	A powerful Cross-Platform C++ General Purpose Library that allows you to
	efficiently create high-performance and reliable applications.

	MaxsiFileFormat.cpp
	A class that handles .mff files, which is a simple archive format.

******************************************************************************/

#include "MaxsiLibrary.h"

BeginMaxsiNamespace

MAXSI_DEFINE_IMPLEMENTATION(MaxsiFileFormat, IFileSystem, MAXSI_INTERFACE_DYNAMIC);

MaxsiFileFormat::MaxsiFileFormat()
{
	MFFFileSystem = NULL;
	BlockData = NULL;
}

MaxsiFileFormat::~MaxsiFileFormat()
{
	if ( BlockData != NULL ) { delete[] BlockData; }
}

MaxsiError MaxsiFileFormat::SetDevice(IBufferStreamer* NewMFFFileSystem, FLAGS32 Mode)
{
	if ( NewMFFFileSystem == NULL ) { return MAXSI_ERROR_INVALID_INPUT; }

	MFFFileSystem = NewMFFFileSystem;

	return MAXSI_ERROR_SUCCESS;
}


MaxsiError MaxsiFileFormat::OpenFileSystem()
{
	size_t ActualRead = 0;

	// Read the header of the MFF filesystem.
	MaxsiError	Result	=	MFFFileSystem->ReadFrom(0, (BYTE*)&HeaderData, sizeof(HeaderData), &ActualRead);

	if ( Result != MAXSI_ERROR_SUCCESS ) { return Result; }

	// Test if we read the entire header.
	if ( ActualRead != sizeof(HeaderData) ) { return MAXSI_ERROR_INPUT_TOO_SMALL; }

	// Test if we support this version.
	if ( HeaderData.Version != 1 ) { return MAXSI_ERROR_UNSUPPORTED_VERSION; }


	if ( BlockData != NULL ) { delete[] BlockData; }
	BlockData	=	new BYTE[HeaderData.BlockSize];

	if ( BlockData == NULL ) { return MAXSI_ERROR_MEM_ALLOC_FAIL; }

	return MAXSI_ERROR_SUCCESS;
}

MaxsiError MaxsiFileFormat::CreateFileSystem(uint64_t NewNumMetaEntries, uint64_t NewNumBlocks, uint64_t NewBlockSize)
{
	MaxsiError Result;

	// Test if we are initialized yet.
	if ( MFFFileSystem == NULL ) { return MAXSI_ERROR_NOT_INITIALIZED; }

	// Test if the block size is less or equal to the meta information before each block. If that is the case
	// then it would be impossible for a block to actually contain data.
	if ( NewBlockSize <= sizeof(MaxsiFileFormatBlockHeader) ) { return MAXSI_ERROR_INVALID_INPUT; } 

	// Calculate the size of the file system!
	uint64_t FileSystemSize = sizeof(MaxsiFileFormatHeader) + sizeof(MaxsiFileFormatMetaListEntry) * NewNumMetaEntries + NewBlockSize * NewNumBlocks;

	// Now allocate enough room for the entire file system.
	if ( MFFFileSystem->SetSize(FileSystemSize) == false ) { return MAXSI_ERROR_UNSPECIFIED; }
	
	char* MagicHeader = "Application/x-MaxsiFileFormat";

	// Now format the file system's header!
	memset(&HeaderData, 0, sizeof(HeaderData));
	memcpy(HeaderData.MagicNumbers, MagicHeader, strlen(MagicHeader));
	HeaderData.Version = 1;
	HeaderData.MetaListSize = NewNumMetaEntries;
	HeaderData.MetaListUsed = 0;
	HeaderData.BlockSize = NewBlockSize;
	HeaderData.NumBlocks = NewNumBlocks;
	HeaderData.FirstUnusedBlock = 0;

	// Let's write the header!
	if ( ( Result = MFFFileSystem->WriteTo(0, (BYTE*)&HeaderData, sizeof(HeaderData)) ) != MAXSI_ERROR_SUCCESS ) { return Result; }

	// Now format the meta indices!
	memset(&MetaData, 0, sizeof(MetaData));
	MetaData.FileSize = 0;
	MetaData.FileFlags = 0;
	MetaData.FirstBlockId = 0;
	MetaData.NumBlocks = 0;	

	// Now lets write lots of unused meta indices.
	for ( uint64_t I = 0; I < NewNumMetaEntries; I++ )
	{
		if ( ( Result = MFFFileSystem->Write((BYTE*)&MetaData, sizeof(MetaData)) ) != MAXSI_ERROR_SUCCESS ) { return Result; }		
	}

	if ( BlockData != NULL ) { delete[] BlockData; }
	BlockData	=	new BYTE[HeaderData.BlockSize];

	if ( BlockData == NULL ) { return MAXSI_ERROR_MEM_ALLOC_FAIL; }

	MaxsiFileFormatBlockHeader*	BlockHeaderData = (MaxsiFileFormatBlockHeader*)BlockData;

	// Now format the blocks!
	memset(BlockData, 0, HeaderData.BlockSize);
	BlockHeaderData->BelongsToFileId = UINT64_MAX;	

	// Now lets write lots of unused meta indices.
	for ( uint64_t I = 0; I < NewNumBlocks; I++ )
	{
		BlockHeaderData->BlockId = I;
		BlockHeaderData->BlockExtends = NewNumBlocks-I;
		BlockHeaderData->NextBlock = I + 1;

		if ( ( Result = MFFFileSystem->Write(BlockData, HeaderData.BlockSize) ) != MAXSI_ERROR_SUCCESS ) { return Result; }		
	}

	return MAXSI_ERROR_SUCCESS;
}

MaxsiError MaxsiFileFormat::ReadMetaEntry(uint64_t FileId)
{
	MaxsiError Result;
	size_t ActualRead = 0;

	uint64_t Offset = sizeof(MaxsiFileFormatHeader) + sizeof(MaxsiFileFormatMetaListEntry) * FileId;

	// Now read the entry about this file!
	if ( ( Result = MFFFileSystem->ReadFrom(Offset, (BYTE*)&MetaData, sizeof(MetaData), &ActualRead) ) != MAXSI_ERROR_SUCCESS ) { return Result; }

	// Check if we read enough memory.
	if ( ActualRead < sizeof(MetaData) ) { return MAXSI_ERROR_CORRUPT_FILESYSTEM; }

	return Result;
}


MaxsiError MaxsiFileFormat::ReadBlockHeader(uint64_t BlockID)
{
	MaxsiError Result;
	size_t ActualRead = 0;

	uint64_t Offset = sizeof(MaxsiFileFormatHeader) + sizeof(MaxsiFileFormatMetaListEntry) * HeaderData.MetaListSize + HeaderData.BlockSize * BlockID;

	// Now read the entry about this file!
	if ( ( Result = MFFFileSystem->ReadFrom(Offset, (BYTE*)&BlockData, sizeof(MaxsiFileFormatBlockHeader), &ActualRead) ) != MAXSI_ERROR_SUCCESS ) { return Result; }

	// Check if we read enough memory.
	if ( ActualRead < sizeof(MaxsiFileFormatBlockHeader) ) { return MAXSI_ERROR_CORRUPT_FILESYSTEM; }

	return Result;
}

MaxsiError MaxsiFileFormat::ReadBlock(uint64_t BlockID)
{
	MaxsiError Result;
	size_t ActualRead = 0;

	uint64_t Offset = sizeof(MaxsiFileFormatHeader) + sizeof(MaxsiFileFormatMetaListEntry) * HeaderData.MetaListSize + HeaderData.BlockSize * BlockID;

	// Now read the entry about this file!
	if ( ( Result = MFFFileSystem->ReadFrom(Offset, (BYTE*)&BlockData, sizeof(MaxsiFileFormatBlockHeader), &ActualRead) ) != MAXSI_ERROR_SUCCESS ) { return Result; }

	// Check if we read enough memory.
	if ( ActualRead < sizeof(MaxsiFileFormatBlockHeader) ) { return MAXSI_ERROR_CORRUPT_FILESYSTEM; }

	return Result;
}

MaxsiError MaxsiFileFormat::FindFileId(UTF8_t* FilePath, uint64_t* FileId)
{
	MaxsiError Result;

	MAXSI_TODO("This is not very efficient! We need to use a binary search instead.");
	MAXSI_TODO("Performance could also increase if we use caching.");

	for ( uint64_t I = 0; I < HeaderData.MetaListUsed; I++ )
	{
		// Now read the entry about this file!
		if ( ( Result = ReadMetaEntry(I) ) != MAXSI_ERROR_SUCCESS ) { return Result; }

		// Check for buffer overruns.
		if ( strlen_s((char*)MetaData.FileName, MAXSI_FF_FILE_NAME_MAX) == SIZE_MAX ) { return MAXSI_ERROR_CORRUPT_FILESYSTEM; }

		if ( strcmp((char*)FilePath, (char*)MetaData.FileName) == 0 ) { *FileId = I; return MAXSI_ERROR_SUCCESS; }
	}

	return MAXSI_ERROR_FILE_NOT_FOUND;
}

MaxsiError MaxsiFileFormat::AllocateMetaEntry(UTF8_t* FilePath, uint64_t Flags, uint64_t* FileId)
{
	// See if there is room for another entry.
	if ( HeaderData.MetaListUsed >= HeaderData.MetaListSize ) { return MAXSI_ERROR_OUT_OF_MEM; }

	// See if the file name is too long for our file system.
	if ( strlen((char*)FilePath) > MAXSI_FF_FILE_NAME_MAX - 1 ) { return MAXSI_ERROR_INPUT_TOO_BIG; }

	// Find the next unused meta id.
	*FileId		=	HeaderData.MetaListUsed;

	// This is now used.
	HeaderData.MetaListUsed++;

	// Now format the meta index!
	memset(&MetaData, 0, sizeof(MetaData));

	MetaData.FileSize = 0;
	MetaData.FileFlags = Flags;
	MetaData.FirstBlockId = 0;
	MetaData.NumBlocks = 0;	
	memcpy(MetaData.FileName, FilePath, strlen((char*)FilePath));

	MaxsiError	Result;

	// Now write the meta index.
	if ( ( Result = MFFFileSystem->Write((BYTE*)&MetaData, sizeof(MetaData)) ) != MAXSI_ERROR_SUCCESS ) { return Result; }	

	return MAXSI_ERROR_SUCCESS;	
}

MaxsiError MaxsiFileFormat::AllocateFileBlocks(uint64_t FileId, uint64_t FileSize)
{
	MaxsiError Result;

	MAXSI_TODO("There is a much more efficient method to find the shortest chain of blocks that have room for FileSize bytes.");

	// Let's read the meta information about this file.
	if ( ( Result = ReadMetaEntry(FileId) ) != MAXSI_ERROR_SUCCESS ) { return Result; }
	
	if ( MetaData.FileSize == FileSize )
	{
		// Funnily, this is actually the size we already wanted it to be. Now quit.
		return MAXSI_ERROR_SUCCESS;
	}

	// Check if we have to make the file shorter, or whatnot.
	if ( MetaData.FileSize < FileSize )
	{
		// Yep! Now figure out how many blocks we need to keep, and how many to delete.
		uint64_t	NumBlocksToKeep		=	DivUp(FileSize, HeaderData.BlockSize);
		uint64_t	CurrentNumBlocks	=	DivUp(MetaData.FileSize, HeaderData.BlockSize);
		uint64_t	CurrentBlockId		=	MetaData.FirstBlockId;

		for ( uint64_t I = 0; I < CurrentNumBlocks; I++ )
		{
			// Read the header of this block!
			ReadBlockHeader(I);
			MaxsiFileFormatBlockHeader*	BlockHeaderData = (MaxsiFileFormatBlockHeader*)BlockData;

			if ( I <= NumBlocksToKeep )
			{
				// Test for extended blocks!
				if ( BlockHeaderData->BlockExtends ){ I += BlockHeaderData->BlockExtends - 1; }

				// Go to the next block!
				CurrentBlockId	=	BlockHeaderData->NextBlock;	
			}

			// TODO! We are NOT done here!
		}
	}

	// Let's seek to the end of FileId.

	return MAXSI_ERROR_SUCCESS;
}

MaxsiError MaxsiFileFormat::GetErrorCode()
{
	return MAXSI_ERROR_NOT_IMPLEMENTED;
}

MaxsiError MaxsiFileFormat::SetErrorCode(MaxsiError NewErrorCode)
{
	return MAXSI_ERROR_NOT_IMPLEMENTED;
}

MaxsiHandle MaxsiFileFormat::OpenFile(MESTR* FileName, FLAGS32 Mode)
{
	return NULL;
}

size_t MaxsiFileFormat::ReadFile(MaxsiHandle FileHandle, BYTE* Buffer, size_t Size)
{
	return 0;
}

size_t MaxsiFileFormat::WriteFile(MaxsiHandle FileHandle, BYTE* Buffer, size_t Size)
{
	return 0;
}

bool MaxsiFileFormat::SeekFile(MaxsiHandle FileHandle, FileSize_t Offset)
{
	return false;
}

bool MaxsiFileFormat::SetFileSize(MaxsiHandle FileHandle, FileSize_t FileSize)
{
	return false;
}

FileSize_t MaxsiFileFormat::GetFileSize(MaxsiHandle FileHandle)
{
	return 0;
}

bool MaxsiFileFormat::CloseFile(MaxsiHandle FileHandle)
{
	return false;
}

bool MaxsiFileFormat::CreateFolder(MESTR* FileName)
{
	return false;
}

bool MaxsiFileFormat::CompareFileNames(MESTR* FileName1, MESTR* FileName2)
{
	return false;
}

MaxsiError MaxsiFileFormat::OpenFolder(MESTR* FolderName, MaxsiDirectoryEntry* Entry)
{
	return MAXSI_ERROR_NOT_IMPLEMENTED;
}

MaxsiError MaxsiFileFormat::ReadFolder(MaxsiDirectoryEntry* Entry)
{
	return MAXSI_ERROR_NOT_IMPLEMENTED;
}

void MaxsiFileFormat::CloseFolder(MaxsiDirectoryEntry* Entry)
{
	return;
}

EndMaxsiNamespace

